#pragma once

#include <chrono>
#include <string>
#include <sstream>
#include <vector>
#include <iterator>

namespace Bosma
{
    using Clock = std::chrono::system_clock;
    
    inline void add(std::tm &tm, Clock::duration time)
    {
        auto tp = Clock::from_time_t(std::mktime(&tm));
        auto tp_adjusted = tp + time;
        auto tm_adjusted = Clock::to_time_t(tp_adjusted);
        tm = *std::localtime(&tm_adjusted);
    }

    class BadCronException : public std::exception
    {
    public:
        explicit BadCronException(std::string msg) : msg_(std::move(msg)) {}

        const char * what() const noexcept override { return msg_.c_str(); }
    private:
        std::string msg_;
    };

    inline void verify_and_set(const std::string & token, const std::string &expression, int &field,
        const int lower_bound,const int upper_bound, const bool adjust = false)
        {
            if (token == "*")
            {
                field = -1;
            }
            else
            {
                try
                {
                    field = std::stoi(token);
                }
                catch(const std::invalid_argument &)
                {
                    throw BadCronException("malformed cron string (`" + token + "` not an integer or *): " + expression);
                }
                catch(const std::out_of_range &)
                {
                    throw BadCronException("malformed cron string (`" + token + "` not convertable to int): " + expression);
                }

                if (field < lower_bound || field > upper_bound)
                {
                    std::ostringstream oss;
                    oss << "malformed cron string ('" << token << "' must be <= " << upper_bound << " and >= " << lower_bound
<< "): " << expression;
                    throw BadCronException(oss.str());
                }

                if (adjust)
                {
                    field--;
                }
            }
        }
    
    class Cron {
    public:
        explicit Cron(const std::string &expression)
        {
            std::istringstream iss(expression);
            std::vector<std::string> tokens { std::istream_iterator<std::string>{iss},
                                              std::istream_iterator<std::string>{} };
            
            if (tokens.size() != 5)
            {
                throw BadCronException("malformed cron string (must be 5 field): " + expression);
            }

            verify_and_set(tokens[0], expression, minute, 0, 59);
            verify_and_set(tokens[1], expression, hour, 0, 23);
            verify_and_set(tokens[2], expression, day, 1, 31);
            verify_and_set(tokens[3], expression, month, 1, 12, true);
            verify_and_set(tokens[4], expression,day_of_week, 0, 6);
        }

        Clock::time_point cron_to_next(const Clock::time_point from = Clock::now()) const
        {
            // get current time as a tm object
            auto now = Clock::to_time_t(from);
            std::tm next(*std::localtime(&now));

            // it will always at least run the next minute
            next.tm_sec = 0;
            add(next, std::chrono::minutes(1));

            while(true)
            {
                if (month != -1 && next.tm_mon != month)
                {
                    // add a month
                    // if this will bring us over a year, increment the year instead and reset the month
                    if (next.tm_mon + 1 > 12)
                    {
                        next.tm_mon = 0;
                        next.tm_year++;
                    }
                    else
                    {
                        next.tm_mon++;
                    }

                    next.tm_mday = 1;
                    next.tm_hour = 0;
                    next.tm_min = 0;
                    continue;
                }

                if (day != -1 && next.tm_mday != day)
                {
                    add(next, std::chrono::hours(24));
                    next.tm_hour = 0;
                    next.tm_min = 0;
                    continue;
                }

                if (day_of_week != -1 && next.tm_wday != day_of_week)
                {
                    add(next, std::chrono::hours(24));
                    next.tm_hour = 0;
                    next.tm_min = 0;
                    continue;
                }

                if (hour != -1 && next.tm_hour != hour)
                {
                    add(next, std::chrono::hours(1));
                    next.tm_min = 0;
                    continue;
                }

                if (minute != -1 && next.tm_min != minute)
                {
                    add(next, std::chrono::minutes(1));
                    continue;
                }
                break;
            }

            // telling mktime to figure out dst
            next.tm_isdst = -1;
            return Clock::from_time_t(std::mktime(&next));
        }
    private:
        int minute;
        int hour;
        int day;
        int month;
        int day_of_week;
    };
}